package com.api.predict.service.impl;

import com.api.base.utils.PageInfoUtils;
import com.api.core.service.AbstractService;
import com.api.predict.dao.PredictMapper;
import com.api.predict.dao.PredictRegistrationMapper;
import com.api.predict.dao.PredictTeamMapper;
import com.api.predict.form.CreatePredictForm;
import com.api.predict.model.Predict;
import com.api.predict.model.PredictRegistration;
import com.api.predict.model.PredictTeam;
import com.api.predict.service.PredictService;
import com.api.predict.vo.PredictRegistrationVo;
import com.api.predict.vo.PredictResultVo;
import com.api.predict.vo.PredictTeamVo;
import com.api.predict.vo.PredictVo;
import com.api.user.dao.UserInfoMapper;
import com.api.user.exception.CoinChangedException;
import com.api.user.exception.CoinNotEnoughException;
import com.api.user.model.CoinTransaction;
import com.api.user.model.UserInfo;
import com.api.user.service.CoinTransactionService;
import com.api.user.service.GameService;
import com.api.user.service.UserInfoService;
import com.api.user.vo.GameVo;
import com.api.user.vo.UserInfoVo;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import tk.mybatis.mapper.entity.Condition;
import tk.mybatis.mapper.entity.Example;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @author RenQiang
 * @date 2021/6/5
 */
@Service
@Transactional(rollbackFor = Exception.class)
public class PredictServiceImpl extends AbstractService<Predict> implements PredictService {
    private final PredictMapper predictMapper;
    private final PredictRegistrationMapper predictRegistrationMapper;
    private final PredictTeamMapper predictTeamMapper;
    private final UserInfoMapper userInfoMapper;
    private final GameService gameService;
    private final UserInfoService userInfoService;
    private final CoinTransactionService coinTransactionService;

    public PredictServiceImpl(PredictMapper predictMapper,
                              PredictRegistrationMapper predictRegistrationMapper,
                              PredictTeamMapper predictTeamMapper,
                              UserInfoMapper userInfoMapper,
                              GameService gameService,
                              UserInfoService userInfoService,
                              CoinTransactionService coinTransactionService) {
        this.predictMapper = predictMapper;
        this.predictRegistrationMapper = predictRegistrationMapper;
        this.predictTeamMapper = predictTeamMapper;
        this.userInfoMapper = userInfoMapper;
        this.gameService = gameService;
        this.userInfoService = userInfoService;
        this.coinTransactionService = coinTransactionService;
    }

    @Override
    public PageInfo<PredictVo> listByPage(Map<String, Object> queryParams, Map<String, String> orderParams, Integer pageNum, Integer pageSize) {
        PageHelper.startPage(pageNum, pageSize);
        List<Predict> predicts = predictMapper.list(queryParams, orderParams);
        PageInfo<Predict> pageInfo = new PageInfo<>(predicts);
        if (CollectionUtils.isEmpty(pageInfo.getList())) {
            return PageInfoUtils.convertPageInfo(pageInfo, Collections.emptyList());
        }

        List<Long> predictIds = predicts.stream()
                .map(Predict::getId)
                .collect(Collectors.toList());
        List<Map<String, Object>> registeredPersons = predictMapper.batchSelectRegisteredPersons(predictIds);
        Map<Long, Integer> registeredPersonsMap = registeredPersons.stream()
                .collect(Collectors.toMap(map -> ((Number) map.get("key")).longValue(), map -> ((Number) map.get("value")).intValue()));
        for (Long predictId : predictIds) {
            registeredPersonsMap.putIfAbsent(predictId, 0);
        }

        List<Long> gameIds = predicts.stream()
                .map(Predict::getGameId)
                .distinct()
                .collect(Collectors.toList());
        Map<Long, GameVo> gameIdMap = gameService.getIdVoMap(gameIds);

        List<PredictVo> predictVos = predicts.stream()
                .map(predict -> {
                    PredictVo predictVo = new PredictVo();
                    BeanUtils.copyProperties(predict, predictVo);
                    predictVo.setRegisteredPersons(registeredPersonsMap.get(predict.getId()));
                    predictVo.setTotalPrize(calcTotalPrize(predict, registeredPersonsMap.get(predict.getId())));
                    predictVo.setGame(gameIdMap.get(predict.getGameId()));
                    return predictVo;
                })
                .collect(Collectors.toList());
        return PageInfoUtils.convertPageInfo(pageInfo, predictVos);
    }

    /**
     * 计算总奖金
     *
     * @param predict
     * @param registeredPersons
     * @return
     */
    public static BigDecimal calcTotalPrize(Predict predict, int registeredPersons) {
        return predict.getPrizeAmount()
                .add(predict.getRegistrationFee()
                        .multiply(BigDecimal.ONE.subtract(predict.getCommissionRate()))
                        .multiply(new BigDecimal(registeredPersons)));
    }

    @Override
    public void registry(Long userId, Long predictId, List<Long> predictTeamIds) throws CoinChangedException {
        Predict predict = predictMapper.selectByPrimaryKey(predictId);
        if (predict == null) {
            throw new IllegalArgumentException("Predict and win not exist");
        }
        if (predict.getCompleted() || predict.getEndTime().before(new Date())) {
            throw new IllegalArgumentException("竞猜已结束");
        }
        if (predict.getStartTime().after(new Date())) {
            throw new IllegalArgumentException("竞猜未开始");
        }

        UserInfo userInfo = userInfoMapper.selectByPrimaryKey(userId);
        if (userInfo == null) {
            throw new IllegalArgumentException("User does not exist");
        }
        if (userInfo.getCoin() == null || userInfo.getCoin().compareTo(predict.getRegistrationFee()) < 0) {
            throw new IllegalArgumentException("金币不足");
        }

        List<PredictTeam> predictTeams = findTeamByIds(predictId, predictTeamIds);
        if (predictTeamIds.size() != predictTeams.size()) {
            throw new IllegalArgumentException("竞猜队伍ID错误");
        }

        CoinTransaction coinTransaction = new CoinTransaction();
        coinTransaction.setFromUserId(userInfo.getUserId());
        coinTransaction.setOrderId(predictId);
        coinTransaction.setAmountBalance(predict.getRegistrationFee());
        coinTransaction.setTransactionType(10);
        coinTransaction.setTransactionTime(System.currentTimeMillis());
        coinTransaction.setRemark("Predict and win");
        try {
            coinTransactionService.batchTransaction(Collections.singletonList(coinTransaction));
        } catch (CoinNotEnoughException e) {
            throw new IllegalArgumentException("用户金币不足");
        }

        PredictRegistration registration = new PredictRegistration();
        registration.setUserId(userId);
        registration.setPredictId(predictId);
        registration.setPredictValue(predictTeamIds.stream()
                .map(Objects::toString)
                .collect(Collectors.joining(",")));
        registration.setFee(predict.getRegistrationFee());
        registration.setBeforeRegistryCoin(userInfo.getCoin());
        BigDecimal afterCoin = userInfo.getCoin().subtract(predict.getRegistrationFee());
        registration.setAfterRegistryCoin(afterCoin);
        Date now = new Date();
        registration.setCreateTime(now);
        registration.setUpdateTime(now);
        predictRegistrationMapper.insert(registration);
    }

    @Override
    public void createNewPredict(CreatePredictForm form) {
        Preconditions.checkArgument(!CollectionUtils.isEmpty(form.getPredictTeams()), "竞猜队伍不能为空");
        Preconditions.checkArgument(form.getPredictTeamCount() <= form.getPredictTeams().size(), "竞猜队伍数量不能大于可选队伍数量");

        Date now = new Date();
        Predict predict = new Predict();
        BeanUtils.copyProperties(form, predict);
        predict.setCompleted(false);
        predict.setCreateTime(now);
        predict.setUpdateTime(now);
        predictMapper.insert(predict);
        List<PredictTeam> predictTeams = form.getPredictTeams().stream()
                .map(teamForm -> convertPredictTeamFormToPo(predict.getId(), teamForm))
                .collect(Collectors.toList());
        predictTeamMapper.insertList(predictTeams);
    }

    @Override
    public void complete(Long predictId, List<Long> victoryTeamIds) throws CoinChangedException {
        Predict predict = predictMapper.selectByPrimaryKey(predictId);
        if (predict == null) {
            throw new IllegalArgumentException("Predict and win not exist");
        }
        if (predict.getPredictTeamCount() != victoryTeamIds.size()) {
            throw new IllegalArgumentException(String.format("胜利组队伍数量错误，需要设置%d个胜利队伍", predict.getPredictTeamCount()));
        }
        if (predict.getCompleted()) {
            throw new IllegalArgumentException("竞猜已结束");
        }

        List<PredictTeam> victoryTeams = findTeamByIds(predictId, victoryTeamIds);
        if (victoryTeams.size() != victoryTeamIds.size()) {
            throw new IllegalArgumentException("胜利组队伍ID错误");
        }
        for (PredictTeam victoryTeam : victoryTeams) {
            victoryTeam.setVictoryRank(victoryTeamIds.indexOf(victoryTeam.getId()) + 1);
            predictTeamMapper.updateByPrimaryKeySelective(victoryTeam);
        }
        predict.setCompleted(true);
        predict.setUpdateTime(new Date());
        predictMapper.updateByPrimaryKeySelective(predict);

        // 用户结算
        Condition condition = new Condition(PredictRegistration.class);
        condition.createCriteria().andEqualTo("predictId", predictId);
        List<PredictRegistration> predictRegistrations = predictRegistrationMapper.selectByCondition(condition);
        if (CollectionUtils.isEmpty(predictRegistrations)) {
            return;
        }
        // 总奖金
        BigDecimal totalPrize = calcTotalPrize(predict, predictRegistrations.size());
        String predictValue = victoryTeamIds.stream()
                .map(Objects::toString)
                .collect(Collectors.joining(","));
        // 猜对人数
        long winUserCount = predictRegistrations.stream()
                .filter(pr -> pr.getPredictValue().equals(predictValue))
                .count();
        // 平均奖金
        BigDecimal averagePrize = winUserCount == 0L ? BigDecimal.ZERO
                : totalPrize.divide(new BigDecimal(winUserCount), RoundingMode.HALF_UP);
        String userIds = predictRegistrations.stream()
                .map(PredictRegistration::getUserId)
                .map(Objects::toString)
                .collect(Collectors.joining(","));
        List<UserInfo> userInfos = userInfoMapper.selectByIds(userIds);
        Map<Long, UserInfo> userIdMap = userInfos.stream()
                .collect(Collectors.toMap(UserInfo::getUserId, Function.identity()));
        for (PredictRegistration predictRegistration : predictRegistrations) {
            if (predictRegistration.getPredictValue().equals(predictValue)) {
                CoinTransaction coinTransaction = new CoinTransaction();
                coinTransaction.setToUserId(predictRegistration.getUserId());
                coinTransaction.setOrderId(predictId);
                coinTransaction.setAmountBalance(averagePrize);
                coinTransaction.setTransactionType(11);
                coinTransaction.setTransactionTime(System.currentTimeMillis());
                coinTransaction.setRemark("竞猜获胜");
                try {
                    coinTransactionService.batchTransaction(Collections.singletonList(coinTransaction));
                } catch (CoinNotEnoughException e) {
                    throw new IllegalArgumentException("用户金币不足");
                }
                predictRegistration.setBeforeCompletedCoin(userIdMap.get(predictRegistration.getUserId()).getCoin());
                predictRegistration.setAfterCompletedCoin(userIdMap.get(predictRegistration.getUserId()).getCoin().add(averagePrize));
                predictRegistration.setWinCoin(averagePrize);
            } else {
                predictRegistration.setBeforeCompletedCoin(userIdMap.get(predictRegistration.getUserId()).getCoin());
                predictRegistration.setAfterCompletedCoin(userIdMap.get(predictRegistration.getUserId()).getCoin());
                predictRegistration.setWinCoin(BigDecimal.ZERO);
            }
            predictRegistrationMapper.updateByPrimaryKeySelective(predictRegistration);
        }
    }

    @Override
    public List<PredictTeamVo> getPredictTeams(Long predictId) {
        Condition condition = new Condition(PredictTeam.class);
        condition.createCriteria()
                .andEqualTo("predictId", predictId);
        List<PredictTeam> predictTeams = predictTeamMapper.selectByCondition(condition);

        List<Map<String, Object>> mapList = predictMapper.selectTeamRegistrationPersons(predictId);
        Map<Long, Integer> teamPersonsMap = mapList.stream()
                .collect(Collectors.toMap(map -> ((Number) map.get("key")).longValue(), map -> ((Number) map.get("value")).intValue()));

        return predictTeams.stream()
                .map(predictTeam -> convertPredictTeamPoToVo(predictTeam,
                        Optional.ofNullable(teamPersonsMap.get(predictTeam.getPredictId())).orElse(0)))
                .collect(Collectors.toList());
    }

    public static PredictTeamVo convertPredictTeamPoToVo(PredictTeam predictTeam, Integer choosePersons) {
        PredictTeamVo teamVo = new PredictTeamVo();
        BeanUtils.copyProperties(predictTeam, teamVo);
        teamVo.setChoosePersons(choosePersons);
        return teamVo;
    }

    @Override
    public List<PredictRegistrationVo> getPredictRegistrations(Long predictId) {
        Condition condition = new Condition(PredictRegistration.class);
        condition.createCriteria()
                .andEqualTo("predictId", predictId);
        List<PredictRegistration> predictRegistrations = predictRegistrationMapper.selectByCondition(condition);
        if(CollectionUtils.isEmpty(predictRegistrations)){
            return  new ArrayList<>();
        }
        // 战队信息
        String predictTeamIds = predictRegistrations.stream()
                .flatMap(po -> Arrays.stream(po.getPredictValue().split(",")))
                .distinct()
                .collect(Collectors.joining(","));

        List<PredictTeam> predictTeams = predictTeamMapper.selectByIds(predictTeamIds);
        Map<Long, PredictTeam> teamIdMap = predictTeams.stream()
                .collect(Collectors.toMap(PredictTeam::getId, Function.identity()));

        // 用户信息
        List<Long> userIds = predictRegistrations.stream()
                .map(PredictRegistration::getUserId)
                .distinct()
                .collect(Collectors.toList());
        if(CollectionUtils.isEmpty(userIds)){
            return  new ArrayList<>();
        }
        Map<Long, UserInfoVo> userIdMap = userInfoService.getIdVoMap(userIds);
        return predictRegistrations.stream()
                .map(registration -> {
                    PredictRegistrationVo registrationVo = new PredictRegistrationVo();
                    registrationVo.setUserInfo(userIdMap.get(registration.getUserId()));
                    List<PredictTeamVo> predictTeamVos = Arrays.stream(registration.getPredictValue().split(","))
                            .map(Long::parseLong)
                            .map(predictTeamId ->
                                    convertPredictTeamPoToVo(teamIdMap.get(predictTeamId), null))
                            .collect(Collectors.toList());
                    registrationVo.setPredictTeams(predictTeamVos);
                    registrationVo.setFee(registration.getFee());
                    registrationVo.setCreateTime(registration.getCreateTime());
                    registrationVo.setWinCoin(registration.getWinCoin());
                    return registrationVo;
                }).collect(Collectors.toList());
    }

    @Override
    public PredictResultVo getPredictResult(Long predictId, Long userId) {
        Preconditions.checkArgument(predictId != null, "参数predictId不能为空");
        Preconditions.checkArgument(userId != null, "参数userId不能为空");

        Predict predict = predictMapper.selectByPrimaryKey(predictId);
        Preconditions.checkArgument(predict != null, "Predict and win not exist");

        PredictResultVo predictResultVo = new PredictResultVo();
        predictResultVo.setPredictId(predictId);
        predictResultVo.setCompleted(predict.getCompleted());
        List<PredictTeam> predictTeams = findPredictTeam(ImmutableMap.of("predictId", predictId));
        predictResultVo.setPredictTeams(predictTeams.stream()
                .map(predictTeam -> convertPredictTeamPoToVo(predictTeam, null))
                .collect(Collectors.toList()));
        if (predict.getCompleted()) {
            predictResultVo.setVictoryTeamIds(predictTeams.stream()
                    .filter(pt -> pt.getVictoryRank() != null)
                    .sorted(Comparator.comparing(PredictTeam::getVictoryRank))
                    .map(PredictTeam::getId)
                    .collect(Collectors.toList()));
        }
        List<PredictRegistration> predictRegistrations = findPredictRegistration(ImmutableMap.of("predictId", predictId, "userId", userId));
        predictResultVo.setRegistrationResults(predictRegistrations.stream()
                .map(pr -> {
                    PredictResultVo.RegistrationResultVo registrationResultVo = new PredictResultVo.RegistrationResultVo();
                    List<Long> predictValueTeams = Arrays.stream(pr.getPredictValue().split(","))
                            .map(Long::parseLong)
                            .collect(Collectors.toList());
                    registrationResultVo.setRegistrationTeams(predictTeams.stream()
                            .filter(t -> predictValueTeams.contains(t.getId()))
                            .sorted(Comparator.comparing(t -> predictValueTeams.indexOf(t.getId())))
                            .map(predictTeam -> convertPredictTeamPoToVo(predictTeam, null))
                            .collect(Collectors.toList()));
                    registrationResultVo.setWinCoins(pr.getWinCoin());
                    return registrationResultVo;
                })
                .collect(Collectors.toList()));
        return predictResultVo;
    }

    @Override
    public void deletePredict(Long predictId) {
        Predict predict = predictMapper.selectByPrimaryKey(predictId);
        Preconditions.checkArgument(predict != null, "Predict and win not exist");
        Preconditions.checkArgument(predict.getStartTime().after(new Date()), "竞猜已开始");
        predictMapper.deleteByPrimaryKey(predictId);
        this.deletePredictTeamsByPredictId(predictId);
    }

    @Override
    public void updatePredict(CreatePredictForm form) {
        Preconditions.checkArgument(form.getId() != null, "参数竞猜ID不能为空");
        Preconditions.checkArgument(!CollectionUtils.isEmpty(form.getPredictTeams()), "Predict teams can't be empty!");
        Preconditions.checkArgument(form.getPredictTeamCount() <= form.getPredictTeams().size(), "竞猜队伍数量不能大于可选队伍数量");

        Predict predict = predictMapper.selectByPrimaryKey(form.getId());
        Preconditions.checkArgument(predict != null, "Predict and win not exist");
        Preconditions.checkArgument(predict.getStartTime().after(new Date()), "竞猜已开始");
        BeanUtils.copyProperties(form, predict);
        predict.setUpdateTime(new Date());
        predictMapper.updateByPrimaryKeySelective(predict);

        this.deletePredictTeamsByPredictId(predict.getId());
        List<PredictTeam> predictTeams = form.getPredictTeams().stream()
                .map(teamForm -> convertPredictTeamFormToPo(predict.getId(), teamForm))
                .collect(Collectors.toList());
        predictTeamMapper.insertList(predictTeams);
    }

    private void deletePredictTeamsByPredictId(Long predictId) {
        Condition condition = new Condition(PredictTeam.class);
        condition.createCriteria()
                .andEqualTo("predictId", predictId);
        predictTeamMapper.deleteByCondition(condition);
    }

    private static PredictTeam convertPredictTeamFormToPo(Long predictId, CreatePredictForm.PredictTeamForm form) {
        PredictTeam predictTeam = new PredictTeam();
        BeanUtils.copyProperties(form, predictTeam);
        predictTeam.setPredictId(predictId);
        return predictTeam;
    }

    private List<PredictTeam> findPredictTeam(Map<String, Object> conditionMap) {
        Condition condition = buildCondition(PredictTeam.class, conditionMap);
        return predictTeamMapper.selectByCondition(condition);
    }

    private List<PredictRegistration> findPredictRegistration(Map<String, Object> conditionMap) {
        Condition condition = buildCondition(PredictRegistration.class, conditionMap);
        return predictRegistrationMapper.selectByCondition(condition);
    }

    static Condition buildCondition(Class<?> entityClass, Map<String, Object> conditionMap) {
        Condition condition = new Condition(entityClass);
        Example.Criteria criteria = condition.createCriteria();
        conditionMap.forEach((key, value) -> {
            if (value == null) {
                criteria.andIsNull(key);
            } else if (Collection.class.isAssignableFrom(value.getClass())) {
                criteria.andIn(key, (Collection<?>) value);
            } else {
                criteria.andEqualTo(key, value);
            }
        });
        return condition;
    }

    private List<PredictTeam> findTeamByIds(Long predictId, Collection<Long> teamIds) {
        Condition condition = new Condition(PredictTeam.class);
        condition.createCriteria()
                .andEqualTo("predictId", predictId)
                .andIn("id", teamIds);
        return predictTeamMapper.selectByCondition(condition);
    }
}
